# [十五] Spring MVC和嵌入式Tomcat

# Spring MVC

导读

Spring MVC是Spring框架的一部分,Spring MVC 是基于 Servlet规范来完成的一个请求响应模块,也是 spring 中比较大的一个模块,现在基本上都是零 xml 配置了,采用的是约定大于配置的方式,所以我们的 Spring MVC 也是采用这种零 xml 配置的方式。要完成这种过程,要解决两个问题 :

  • 1、取代 web.xml 配置
  • 2、取代 springmvc.xml 配置

# Spring MVC 加载过程

1、首先,完成了listenerDispatcherServlet的创建和实例化

入口类: SpringServletContainerInitializer # onStartup

  • ├─ onStartup Spring实现Servlet的核心入口
  • ├─ onStartup 实现WebApplicationInitializer接口中的方法
  • │ ├─ super.onStartup ① 在父类中完成了实例化 监听 的工作 父容器
  • │ │ └─ registerContextLoaderListener ② 创建 spring上下、监听器,把监听器添加到 servlet 上下文
  • │ └─ registerDispatcherServlet ③ 注册实例化 DispatcherServlet 子容器
  • │ │ ├─ createServletApplicationContext ④ 创建 springmvc的上下文,注册MvcContainer类
  • │ │ ├─ createDispatcherServlet ⑤ 创建 DispatcherServlet
  • │ │ └─ addServlet ⑥ 把 DispatcherServlet添加到 servlet上下文

2、父容器的启动,由Tomcat负责调用

入口类:StandardContext#listenerStart

  • ├─ listenerStart ① Tomcat启动的时候调用到的初始化方法
  • │ ├─ contextInitialized ② Spring中调用到的初始化方法
  • │ │ └─ initWebApplicationContext ③ 内部初始化方法,完成Spring容器的启动
  • │ │ │ ├─ configureAndRefreshWebApplicationContext ④ 启动 Spring 容器入口
  • │ │ │ │ └─ refresh ⑤ 启动Spring 容器 父容器启动
  • │ │ │ └─ servletContext.setAttribute ⑥ 把上下文对象设置到 servletContext 中

3、子容器的启动,由Servlet负责调用 入口类:HttpServletBean#init

  • ├─ init ① Servlet 的核心初始化方法
  • │ ├─ initServletBean ② 启动 DispatcherServlet 入口
  • │ │ └─ initWebApplicationContext ③ 启动 DispatcherServlet 入口
  • │ │ │ ├─ configureAndRefreshWebApplicationContext ④ 启动 DispatcherServlet 入口
  • │ │ │ │ └─ refresh ⑤ 启动DispatcherServlet子 容器 子容器启动
  • │ │ │ └─ onRefresh ⑥ DispatcherServlet初始化,模板方法
  • │ │ │ │ └─ initStrategies ⑦ 初始化策略内容
  • │ │ │ │ │ ├─ initMultipartResolver ⑧ 初始化文件上传处理
  • │ │ │ │ │ ├─ initLocaleResolver ⑨ 初始化本地化Resolver
  • │ │ │ │ │ ├─ initThemeResolver ⑩ 初始化主题Resolver
  • │ │ │ │ │ ├─ initHandlerMappings ⑪ 初始化URL映射关系
  • │ │ │ │ │ ├─ initHandlerAdapters ⑫ 初始化Handler接口适配器
  • │ │ │ │ │ ├─ initHandlerExceptionResolvers ⑬ 初始化异常处理的handler
  • │ │ │ │ │ ├─ initRequestToViewNameTranslator ⑭ 初始化请求路径转换
  • │ │ │ │ │ ├─ initViewResolvers ⑮ 火初始化视图解析
  • │ │ │ │ │ └─ initFlashMapManager ⑯ 初始化Flashmap管理

# 取代 web.xml 配置

# ServletContainerInitializer

概述

Servlet 3.0引入的接口,用于在web应用启动时动态添加ServletFilterListenerServlet 3.0 +的容器将自动扫描类路径,就是当 Servlet 容器启动的时候会根据 SPI规范加载META-INF/services/javax.servlet.ServletContainerInitializer文件路径中存放的实现该接口的所有类。

  • 文件路径

  • 文件内容
org.springframework.web.SpringServletContainerInitializer

# SpringServletContainerInitializer

概述

SpringServletContainerInitializer是Spring中实现ServletContainerInitializer接口的唯一类,也是核心类,重点掌握;

  • SpringServletContainerInitializer 源码
	@HandlesTypes(WebApplicationInitializer.class)
	public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {
		// 需要初始化的 WebApplicationInitializer类型的容器
		List<WebApplicationInitializer> initializers = new LinkedList<>();
		// 所有实现了 @HandlesTypes 注解中 WebApplicationInitializer接口类型的类
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				// 如果 这些实现类不是接口、不是抽象类、并且是 WebApplicationInitializer的子类
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						// 通过反射示例化,并添加到初始化容器中
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		// 实例化完成后,循环调用 onStartup 方法
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

知识点

/*
 * 所在类:org.apache.catalina.util.LifecycleBase
 */
public final synchronized void start() throws LifecycleException {
	 this.startInternal();
}
/*
 * 所在类:org.apache.catalina.core.StandardContext
 */
 protected synchronized void startInternal() throws LifecycleException {
  // 在转为ServletContainerInitializer类型时候,会调用到实现其接口的类
  // SpringServletContainerInitializer 中的 onStartup方法
   ((ServletContainerInitializer)entry.getKey()).onStartup((Set)entry.getValue(), this.getServletContext());
}

SpringServletContainerInitializer类在Tomcat启动的时候会被 servlet 容器实例化, 然后调用 onStartup 方法,并且 servlet 容器会收集实现了@HandlesTypes注解里面的接口的类,并且做为入参传入到onStartup 方法中,我们拿到 set 容器中的类就可以反射调用接口里面的方法了,这是 servlet 规范,该规范能保证 servlet 容器在启动的时候就会完成这些操作。Spring MVC 就借助这一点完成了取代web.xml 的工作。

# WebApplicationInitializer

Tomcat启动的时候,首先会加载SpringServletContainerInitializer类,接着会按次序javax.annotation.Priority调用所有实现了WebApplicationInitializer接口类的onStartup 方法,


	public interface WebApplicationInitializer {
	// Tomcat 启动最终会执行其实现类中的 onStartup 方法
	void onStartup(ServletContext servletContext) throws ServletException;

}

知识点

WebApplicationInitializer 接口的几个实现类:

  • AbstractContextLoaderInitializer:实现了 WebApplicationInitializer 接口
  • AbstractDispatcherServletInitializer: 继承了 AbstractContextLoaderInitializer
  • AbstractAnnotationConfigDispatcherServletInitializer:继承了AbstractDispatcherServletInitializer
  • SpringBootServletInitializer:实现了 WebApplicationInitializer 接口

WebApplicationInitializer目前只对war启动的项目有效,对jar启动的项目无效。

所以,Spring MVC工程中的AbstractDispatcherServletInitializer类中的onStartup 方法最终就会被调用执行

进入onStartup() 方法

类文件: org.springframework.web.servlet.support.AbstractDispatcherServletInitializer

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		// 在父类中完成了实例化 listener 的工作
		super.onStartup(servletContext);
		// 注册实例化 DispatcherServlet
		registerDispatcherServlet(servletContext);
	}

进入super.onStartup 方法

类文件: org.springframework.web.context.AbstractContextLoaderInitializer

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		registerContextLoaderListener(servletContext);
	}
	
	protected void registerContextLoaderListener(ServletContext servletContext) {
		//创建 spring上下文,注册了 SpringContainer
		WebApplicationContext rootAppContext = createRootApplicationContext();
		if (rootAppContext != null) {
		   /*
			*  TODO : 创建监听器,取代 web.xml中的这种配置:
			* <listener>
			* 	  <listener-class>
			* 	  org.springframework.web.context.ContextLoaderListener
			* 	  </listener-class>
			* </listener>
			*
			* */
			ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
			listener.setContextInitializers(getRootApplicationContextInitializers());
			// 把监听器添加到 servlet 上下文中
			servletContext.addListener(listener);
		}
		else {
			logger.debug("No ContextLoaderListener registered, as " +
					"createRootApplicationContext() did not return an application context");
		}
	}
	

进入createRootApplicationContext 方法

类文件: org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer

protected WebApplicationContext createRootApplicationContext() {
		// 返回AbstractAnnotationConfigDispatcherServletInitializer子类中重写
		// 的getRootConfigClasses方法,该方法通常使用 @ComponentScan负责扫描加载
		// 的扫描配置类(父容器),也就是扫描不包括 @Controller注解的类,钩子方法
		Class<?>[] configClasses = getRootConfigClasses();
		if (!ObjectUtils.isEmpty(configClasses)) {
			AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
			// 配置类不为空,则注册到上下文
			context.register(configClasses);
			return context;
		}
		else {
			return null;
		}
	}

知识点

通过钩子方法getRootConfigClasses获取到扫描类后,注册到了上下文对象中,然后把 spring 的上下文对象设置到了 ContextLoaderListener 监听器对象中,最后把监听器对象设置到了 servletContext 中。这里上下文对象还没有调用 refresh 方法完成 spring 的启动。

返回onStartup方法,

进入registerDispatcherServlet 方法

类文件: org.springframework.web.servlet.support.AbstractDispatcherServletInitializer

/**
 *  TODO : 注册实例化 DispatcherServlet,取代 Web.xml中的配置
 */
protected void registerDispatcherServlet(ServletContext servletContext) {
		String servletName = getServletName();
		Assert.hasLength(servletName, "getServletName() must not return null or empty");
		//创建 springmvc的上下文,注册了 MvcContainer类
		WebApplicationContext servletAppContext = createServletApplicationContext();
		Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
		// 创建 DispatcherServlet
		FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
		Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
		dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

		ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
		if (registration == null) {
			throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
					"Check if there is another servlet registered under the same name.");
		}
		/*
		* 	如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
		* 	如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,
		* 	值越小,servlet的优先级越高,就越先被加载
		* */
		registration.setLoadOnStartup(1);
		registration.addMapping(getServletMappings());
		registration.setAsyncSupported(isAsyncSupported());

		Filter[] filters = getServletFilters();
		if (!ObjectUtils.isEmpty(filters)) {
			for (Filter filter : filters) {
				registerServletFilter(servletContext, filter);
			}
		}

		customizeRegistration(registration);
	}

此处的registerDispatcherServlet方法实则取代了web.xml中的如下配置:

  <servlet>
    <servlet-name>spring-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <!--springmvc的配置文件-->
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-dispatcher.xml</param-value>
    </init-param>
    <load-on-startup>0</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring-dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

知识点

创建上下文对象,跟上面创建监听器差不多,创建 dispatcherServlet 对象,把 servlet 对象加入到 servletContext 上下文中。把上下文对象设置到了dispatcherServlet 对象中了。这里上下文对象仍然没有调用 refresh 方法,没有启动spring 容器。

# Spring 容器的启动

Spring 容器启动的核心方法是contextInitialized,最终在Tomcat的启动过程中,由Tomcat来负责调用执行的。

进入tomcat-embed-core-8.5.23.jart包的listenerStart 方法

类文件: org.apache.catalina.core.StandardContext

public boolean listenerStart() {
// ...... 省略	
for(int i = 0; i < instances.length; ++i) {
	if (instances[i] instanceof ServletContextListener) {
		ServletContextListener listener = (ServletContextListener)instances[i];
		try {
			this.fireContainerEvent("beforeContextInitialized", listener);
			if (this.noPluggabilityListeners.contains(listener)) {
				// 此处会调到 ContextLoaderListener 类的 		   
				// contextInitialized 方法, 完成 Spring 容器的启动
				listener.contextInitialized(tldEvent);
			} else {
				listener.contextInitialized(event);
			}
			this.fireContainerEvent("afterContextInitialized", listener);
		} catch (Throwable var12) {
			ExceptionUtils.handleThrowable(var12);
			this.fireContainerEvent("afterContextInitialized", listener);
			this.getLogger().error(sm.getString("standardContext.listenerStart", new Object[]{instances[i].getClass().getName()}), var12);
			ok = false;
		}
	}
}
// ...... 省略	
 }

进入contextInitialized 方法

类文件: org.springframework.web.context.ContextLoaderListener

public void contextInitialized(ServletContextEvent event) {
		// servlet 容器启动的时候会调用初始化方法,完成Spring容器的启动。
		initWebApplicationContext(event.getServletContext());
	}

进入initWebApplicationContext 方法

类文件: org.springframework.web.context.ContextLoader

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
		// ...... 省略		
		long startTime = System.currentTimeMillis();
		try {
			if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					if (cwac.getParent() == null) {
					ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					// 启动 Spring 容器入口
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
// 把上下文对象设置到 servletContext 中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
		// ...... 省略
	}

进入configureAndRefreshWebApplicationContext 方法

类文件: org.springframework.web.context.ContextLoader

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		// ...... 省略

		customizeContext(sc, wac);
		// 此处最终调用了 AbstractApplicationContext的 refresh 方法启动 
		// Spring容器, 也就是 Spring 容器启动最核心的方法,该监听器启动的Spring容器
    	// 将作为父容器,后面DispatchServlet启动的Spring容器是其子容器。
		wac.refresh();
	}

# DispatcherServlet 的启动

进入init 方法

类文件: org.springframework.web.servlet.HttpServletBean

public final void init() throws ServletException {
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		if (!pvs.isEmpty()) {
			try {
				BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
				ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
				bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
				initBeanWrapper(bw);
				bw.setPropertyValues(pvs, true);
			}
			catch (BeansException ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// 启动 DispatcherServlet 入口
		initServletBean();
	}

进入initServletBean 方法

类文件: org.springframework.web.servlet.FrameworkServlet

	@Override
	protected final void initServletBean() throwss ServletException {
	// 对应日志输出,说明开始 DispatcherServlet 初始化: 
	// INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcher'
		if (logger.isInfoEnabled()) {
			logger.info("Initializing Servlet '" + getServletName() + "'");
		}
		long startTime = System.currentTimeMillis();

		try {
			// 启动 DispatcherServlet 入口
			this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
		catch (ServletException | RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			throw ex;
		}

	}

进入initWebApplicationContext 方法

类文件: org.springframework.web.servlet.FrameworkServlet

	protected WebApplicationContext initWebApplicationContext() {
		// 从 servletContext 中获取父容器
		WebApplicationContext rootContext =
	WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;
		if (this.webApplicationContext != null) {
			wac = this.webApplicationContext;
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				if (!cwac.isActive()) {
					if (cwac.getParent() == null) {
						cwac.setParent(rootContext);
					}
					configureAndRefreshWebApplicationContext(cwac);
				}
			}
		}
		if (wac == null) {
			wac = findWebApplicationContext();
		}
		if (wac == null) {
			wac = createWebApplicationContext(rootContext);
		}

		if (!this.refreshEventReceived) {
			synchronized (this.onRefreshMonitor) {
				// 最终执行 onRefresh 方法完成 DispatcherServlet的启动
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}

知识点

initWebApplicationContext方法主要就是由 listener 负责启动的容器,然后把父容器设置到了自己的上下文对象中,所以这里监听器启动的容器是父容器,dispatcherServlet 启动的容器是子容器,两者是父子关系。

到此为止,就用 servlet 规范完成了取代 web.xml 的工作,并启动了容器。

# 取代 springmvc.xml 配置

概述

我们用一个@EnableWebMvc 就可以完全取代springmvc.xml配置,其实两者完成的工作是一样的, 都是为了创建必要组件的实例,

# @EnableWebMvc

  • @EnableWebMvc的作用其实就是引入一个DelegatingWebMvcConfiguration用于配置Spring MVC。而通常,我们通常使用注解@EnableWebMvc在一个实现了WebMvcConfigurer接口的配置类上或者继承了WebMvcConfigurerAdapter来配置Spring MVC

注意

从Spring 5.0开始,WebMvcConfigurer就有了默认的方法(由Java 8基线实现),并且可以直接实现,而不需要这个WebMvcConfigurerAdapter来适配,并且已经标注为@Deprecated(弃用)。因此官方建议直接实现WebMvcConfigurer接口。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 导入Spring MVC进行配置的一个代理类
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

# DelegatingWebMvcConfiguration

  • DelegatingWebMvcConfiguration本身使用了注解@Configuration,所以它是一个配置类,因此它也会被作为一个单例bean注册到容器和被容器注入相应的依赖。@Autowired说明本身又依赖了WebMvcConfigurer实例对象,说明WebMvcConfigurer也会被注入到Spring 容器中。

进入DelegatingWebMvcConfiguration 方法

类文件: org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

	@Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}
	
	// ...... 省略	
}

# WebMvcConfigurationSupport

  • 这里会导入一个核心类 WebMvcConfigurationSupport,在这个类里面会完成很多组件的实例化,比如 HandlerMapping,HandlerAdapter 等等。然后在实例化过程中会涉及到很多钩子方法的调用,而这些钩子方法就是我们需要去实现的,比如获取拦截器的钩子方法,获取静态资源处理的钩子方法等等。
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
	@Bean
	public RequestMappingHandlerMapping requestMappingHandlerMapping() {
	// ...... 省略
	}
	@Bean
	public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
	// ...... 省略
	}
	@Bean
	@Nullable
	public HandlerMapping viewControllerHandlerMapping() {
		ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
		addViewControllers(registry);

		AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping();
		if (handlerMapping == null) {
			return null;
		}
		handlerMapping.setPathMatcher(mvcPathMatcher());
		handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
		handlerMapping.setInterceptors(getInterceptors());
		handlerMapping.setCorsConfigurations(getCorsConfigurations());
		return handlerMapping;
	}
	// 添加拦截器钩子方法
	protected void addInterceptors(InterceptorRegistry registry) {
	}
	// 添加静态资源钩子方法
	protected void addResourceHandlers(ResourceHandlerRegistry registry) {
	}

}

注意

springboot下自定义的WebMvcConfigurer实现配置类上是不需要添加@EnableWebMvc的,因为springboot已经实例化了WebMvcConfigurationSupport,如果添加了该注解,默认的WebMvcConfigurationSupport配置类是不会生效的,也就是以用户定义的为主,一般建议还是不覆盖默认的好。